문단 클릭 = 시작 위치 선택 · 재생은 우하단 버튼/P (재생 중 클릭 = 점프)

EP0. 로컬 RAG 구축 — Qwen3 임베딩·ChromaDB·인스트럭션 검색과 온프레미스 배포

OpenAI에 의존하지 않고, 로컬에 올린 디코더 기반 임베딩 모델(Qwen3 Embedding 8B)과 ChromaDB만으로 리랭커·BM25 없이도 정확한 RAG 검색을 만들고, Docker·Nginx Proxy Manager로 HTTPS 도메인에 온프레미스 배포합니다.

0. 사전 필수 용어 (선행지식)

이 강의는 "임베딩으로 벡터를 만들고, 벡터 유사도로 문서를 찾고, 그 문서를 LLM에 넣어 답을 만든다"는 RAG의 기본 위에 서 있습니다. 아래 5개 용어를 먼저 잡아두면 §1 이후가 쉽게 읽힙니다.

  1. 임베딩(Embedding) — 텍스트를 숫자 벡터로 바꾼 것. 의미가 비슷한 문장은 벡터 공간에서 가까이 놓입니다. 비유하면 "문장마다 지도 위 좌표를 찍는 일"입니다. §1·§4 Step 1에서 씁니다.
  2. 코사인 유사도(Cosine Similarity) — 두 벡터가 이루는 각도로 유사도를 재는 방법. 값이 1에 가까울수록 방향(=의미)이 같습니다. 길이가 아니라 "방향"만 보므로 문서 길이에 덜 휘둘립니다. §4 Step 2에서 계산합니다.
  3. 인코더 vs 디코더 임베딩 — 인코더 기반은 문장 속 모든 토큰 임베딩의 평균을 벡터로 삼습니다. 디코더 기반(LLM 계열)은 트랜스포머처럼 마지막 히든 스테이트를 벡터로 삼아 문맥의 초점을 이해합니다. §6 핵심 원리의 축입니다.
  4. 벡터 데이터베이스 / ChromaDB — 임베딩 벡터를 저장하고 "가장 가까운 K개"를 빠르게 찾아주는 저장소. ChromaDB는 그중 가장 미니멀한 축에 듭니다. §4 Step 4~6에서 씁니다.
  5. 리버스 프록시 / Nginx Proxy Manager(NPM) — 외부 도메인 요청을 받아 내부 서비스로 전달하고 HTTPS를 붙여주는 관문. NPM은 Nginx를 GUI로 감싸 인증서 발급·프록시를 쉽게 해줍니다. §4 후반 배포와 §9에서 씁니다.

📚 참고: RAG·벡터 검색의 배경이 얕다면 벡터DB 개념부터 보완하세요. 임베딩·검색·인덱스의 표준 설명은 LlamaIndex Docs(https://docs.llamaindex.ai)와 LangChain Docs(https://docs.langchain.com/oss/python/langchain/overview)가 잘 정리해 둡니다.

1. 주제 정의

로컬 RAG는 임베딩·벡터DB·LLM을 모두 자기 컴퓨터(온프레미스)에서 돌려, 외부 API에 데이터를 내보내지 않고 검색·생성을 수행하는 RAG입니다. 이 강의는 그중에서도 "디코더 기반 임베딩 모델 + ChromaDB + 인스트럭션 검색"이라는 조합으로, 구형 RAG 교재의 인코더 임베딩·리랭커·BM25 스택을 대체합니다.

핵심 아이디어: 임베딩 모델을 LLM처럼 다루면(=디코더 기반) 벡터가 문맥을 이해하고, 게다가 인스트럭션(instruction) 으로 "이 쿼리는 가격 상승/리스크/정책 관점에서 봐라"라고 지시할 수 있어 벡터 검색만으로 목적에 맞는 결과가 나옵니다.

2. 풀려는 문제

  • 문제 1 — OpenAI 종속: 임베딩까지 OpenAI에 맡기면 비용·데이터 유출·오프라인 불가 문제가 생깁니다. 강의는 "임베딩도 OpenAI한테서 독립"하는 것을 첫 목표로 삼습니다.
  • 문제 2 — 구형 인코더 임베딩의 저품질: 인코더 기반 임베딩은 토큰 평균이라 "과일 값이 비싸다"에 엉뚱하게 "전통시장"을 매칭합니다. 벡터 유사도 자체가 형편없어 RAG가 잘 안 맞습니다.
  • 문제 3 — 리랭커·BM25 남용: 벡터 품질이 나쁘니 교재들이 리랭커·BM25·시맨틱 청킹 같은 보정 기법을 잔뜩 얹습니다. 이는 근본 원인(임베딩 품질)을 피한 우회입니다.
  • 문제 4 — 온프레미스 배포의 벽: 로컬에서 만든 Streamlit 앱을 외부 도메인·HTTPS로 서비스하려면 DNS·포트포워딩·SSL 인증서가 얽혀 초심자에게 진입장벽이 큽니다.
  • 문제 5 — 자원 한계: 임베딩 8B·LLM 35B를 한 대에서 동시에 올리기엔 VRAM이 부족합니다.

💡 실무 노하우: 문제 2·3은 한 뿌리입니다. 임베딩 품질을 올리면(디코더 기반+인스트럭션) 리랭커·BM25가 "있으면 좋은 것"에서 "대부분 불필요한 것"으로 바뀝니다. 다만 이는 도메인·데이터에 따라 다르니 §9의 한계를 함께 보세요.

3. 핵심 개념·구조

시스템은 다음 요소로 구성됩니다.

  • 임베딩 서버(LM Studio + Qwen3 Embedding 8B): OpenAI 호환 엔드포인트(/v1)를 로컬에 띄워, LangChain의 OpenAIEmbeddings로 그대로 호출합니다. 스펙이 LLM 기반이라 최대 40K 토큰까지 받고 4096차원 벡터를 냅니다.
  • 인스트럭션 레이어: 쿼리에 시스템 프롬프트 성격의 인스트럭션을 붙여 같은 문장을 다른 관점으로 임베딩합니다(가격 상승/리스크/정책 등).
  • 벡터 저장소(ChromaDB, Docker 컨테이너): HNSW 인덱스를 코사인 기준으로 잡아 유사도 검색을 수행합니다.
  • 문서 전처리기(커스텀 PDF 스플리터 + RecursiveCharacterTextSplitter): 머리글·페이지 번호·표/그림만 있는 페이지 등 노이즈를 제거한 뒤 청크로 자릅니다.
  • 생성 LLM(LM Studio, Qwen3 계열 35B 로컬): 검색된 청크를 컨텍스트로 받아 답변을 생성합니다.
  • 서비스/배포(Streamlit + Docker + Nginx Proxy Manager + Route 53 + Let's Encrypt): 챗봇 UI를 컨테이너로 굽고 HTTPS 도메인에 노출합니다.
[사용자] --HTTPS--> [Route 53: *.도메인 A레코드 → 집 공인 IP]
                          |
                    [AP/라우터: 80·443 포트포워딩 → 내 PC]
                          |
             [Nginx Proxy Manager 컨테이너 :80/:443/:81]
                          |  (Proxy Host + Let's Encrypt SSL)
                          v
        +---------------- Docker 내부 네트워크 ----------------+
        | [Streamlit app.py 컨테이너 :8501]                    |
        |        |  as_retriever(k=3)                           |
        |        v                                              |
        | [ChromaDB 컨테이너 (host="chromadb", :8000)]          |
        |        ^  embedding_function                          |
        |        |                                              |
        | [LM Studio /v1] — Qwen3 Embedding 8B(4096d) + LLM 35B |
        +-------------------------------------------------------+

4. 구현 가이드 (Do It Yourself)

시작 전 (Before you begin)

이 섹션을 완료하면 로컬 RAG를 실제 코드로 구현하고, 인스트럭션 검색의 효과를 직접 확인하며, HTTPS 도메인으로 서비스까지 띄울 수 있습니다.

선수 조건: - Python 3.10+ (강의 Docker 이미지는 python:3.11-slim) - RAM 16GB 이상(권장 32GB+), GPU VRAM 8GB 이상 - Docker Desktop, Jupyter Notebook 또는 IDE - LM Studio + Qwen3 Embedding 8B 모델 파일 - 라이브러리: pip install -q langchain-community langchain-text-splitters langchain-openai langchain-chroma chromadb pypdf streamlit - 배포 실습 시: 도메인 1개, AWS 계정(Route 53), AP/라우터 관리자 권한

소요 시간: 약 60~90분.

Step 1 — 로컬 임베딩 서버 연결 (OpenAI 독립)

목표: LM Studio에 올린 Qwen3 Embedding 8B를 LangChain으로 호출합니다.

다음 코드를 practice2-1.ipynb 셀에 추가합니다.

from numpy import dot
from numpy.linalg import norm
import pandas as pd
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="embedding-8b:sl",                       # LM Studio에 뜨는 모델 이름
    base_url="http://host.docker.internal:12345/v1",  # /v1 까지 붙여야 함
    api_key="lm-studio",                           # 아무 값이나 OK
    check_embedding_ctx_length=False,              # 토큰 수 대신 원문 문자열 전송
)

query_result = embeddings.embed_query('저는 배가 고파요')
print(len(query_result))  # 4096

이 코드는 OpenAI 서버가 아니라 로컬 LM Studio의 OpenAI 호환 엔드포인트로 임베딩을 요청합니다. 출력 차원이 4096이면 Qwen3 계열 8B 임베딩이 정상 연결된 것입니다(1536차원인 ada-002가 아님).

⚠️ 주의: API 키를 소스에 직접 박지 마세요. 로컬이라 값이 임의(lm-studio)여도 되지만, 실제 클라우드 SDK로 옮길 때는 환경 변수(예: ANTHROPIC_API_KEY, OPENAI_API_KEY)를 사용합니다. base_url/v1을 빠뜨리면 연결이 실패합니다.

💡 실무 노하우: 8B 임베딩이라도 VRAM 5GB(Q4_K_M 양자화) 정도면 올라갑니다. 임베딩 서버와 생성 LLM을 서로 다른 머신에 분산하면 여러 대의 저사양 GPU로도 운영할 수 있습니다.

📚 참고: OpenAI 호환 임베딩 클라이언트 사용법은 LangChain의 langchain-openai(https://github.com/langchain-ai/langchain)와 OpenAI Python SDK(https://github.com/openai/openai-python)를 참고하세요.

확인: Step 1 완료. print(len(query_result))4096을 출력하면 임베딩 서버 연결 성공.


Step 2 — 코사인 유사도로 기본 검색

목표: 임베딩과 코사인 유사도로 후보 문장을 뽑는 최소 검색기를 만듭니다.

def get_embedding(text):
    return embeddings.embed_query(text)

df['embedding'] = df.apply(lambda row: get_embedding(row.text), axis=1)

def cos_sim(A, B):
    return dot(A, B) / (norm(A) * norm(B))

def return_answer_candidate(df, query):
    query_embedding = get_embedding(query)
    df["similarity"] = df.embedding.apply(
        lambda x: cos_sim(np.array(x), np.array(query_embedding))
    )
    return df.sort_values("similarity", ascending=False).head(3)

sim_result = return_answer_candidate(df, '과일 값이 비싸다')

이 코드는 각 문장을 임베딩해 두고, 질의 임베딩과의 코사인 유사도로 상위 3개를 반환합니다. 강의 결과에서 "과일 값이 비싸다"에 대해 "시장 물가가 올랐어요"가 상위(≈0.71)로 잡힙니다.

💡 실무 노하우: 디코더 임베딩은 "비싸다"라는 초점을 이해하므로, 인코더 모델이 흔히 매칭하던 "전통시장" 같은 표면적 연관어가 사라집니다.

확인: Step 2 완료. sim_result에 물가·가격 관련 문장이 상위로 정렬됩니다.


Step 3 — 인스트럭션 기반 임베딩 (같은 쿼리, 다른 관점)

목표: 쿼리에 인스트럭션을 붙여 검색 관점을 바꿉니다.

def get_detailed_instruct(task_description, query):
    return f'Instruct: {task_description}\nQuery: {query}'

def get_embedding_instruct(task_description, query):
    return get_embedding(get_detailed_instruct(task_description, query))

def return_answer_candidate_instruct(df, task_description, query):
    q_emb = get_embedding_instruct(task_description, query)
    df["similarity"] = df.embedding.apply(
        lambda x: cos_sim(np.array(x), np.array(q_emb))
    )
    return df.sort_values("similarity", ascending=False).head(3)

query = '과일 값이 비싸다'
tasks = {
    "A_generic":   "Retrieve semantically similar sentences.",
    "B_price":     "Retrieve sentences about price increase or cost of living.",
    "C_websearch": "Retrieve passages that could answer this web search query.",
}
for name, task in tasks.items():
    print(name, return_answer_candidate_instruct(df, task, query))

같은 쿼리라도 인스트럭션(A 일반 / B 가격 상승 / C 웹 검색)에 따라 상위 문서와 유사도 점수가 달라집니다. 디코더 기반 모델이 인스트럭션을 시스템 프롬프트처럼 해석하기 때문입니다.

⚠️ 주의: 인스트럭션 문구는 모델 카드가 권장하는 형식(Instruct: ... / Query: ...)을 따르세요. 형식이 어긋나면 인스트럭션 효과가 약해집니다.

확인: Step 3 완료. B(가격) 인스트럭션에서 물가 문장이, C(웹검색)에서 다른 순위가 나오면 인스트럭션이 먹은 것입니다.


Step 4 — ChromaDB Docker 컨테이너 실행

목표: 벡터 저장소를 Docker로 띄우고 하트비트로 살아있음을 확인합니다.

docker run -d --name ChromaDB -v chromadb_data:/chroma chromadb/chroma

컨테이너 이름을 ChromaDB, 볼륨을 chromadb_data로 잡습니다. 노트북(주피터)도 같은 Docker 안에 있으면 포트를 바깥으로 뺄 필요가 없습니다 — 컨테이너 이름이 곧 내부 도메인이 됩니다.

import chromadb
chroma_client = chromadb.HttpClient(host="chromadb", port=8000)
chroma_client.heartbeat()   # ns 값이 나오면 살아있음

⚠️ 주의: host에 IP/URL이 아니라 컨테이너 이름(chromadb)을 줍니다. 외부에서 접근할 때만 포트포워딩이 필요합니다.

💡 실무 노하우: 포트를 열지 않고 내부 네트워크 이름으로만 통신하면 공격 표면이 줄어 더 안전합니다.

확인: Step 4 완료. heartbeat()가 나노초 정수를 반환.


Step 5 — 문서 전처리 + 컬렉션 생성

목표: PDF를 노이즈 제거 후 청크로 나눠 ChromaDB 컬렉션에 임베딩합니다.

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = PyPDFLoader("2024 KB 부동산 보고서.pdf")
pages = loader.load()

# 커스텀 클린업: 머리글·페이지 번호 제거, 표/그림만 있는 페이지 스킵
cleaned = clean_pages(pages)  # 강의 제공 커스텀 코드

splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
splits = splitter.split_documents(cleaned)   # 84페이지 → 215 청크

collection_name = "kb_rag_collection"
# 컬렉션이 있으면 삭제 후 재생성

임베딩 함수는 Step 1의 embeddings를 그대로 지정하고, 인덱스는 HNSW를 코사인 기준으로 잡습니다(내적 아님).

⚠️ 주의: 책의 기본 스플리터는 머리글·페이지 번호를 남겨, 모든 청크에 "국민은행 …" 같은 노이즈가 반복 삽입되어 검색 품질을 떨어뜨립니다. 청킹 전에 노이즈를 먼저 제거하세요.

💡 실무 노하우: collection.count()로 청크 수(예: 215)가 그대로 적재됐는지 확인합니다.

확인: Step 5 완료. collection.count()가 청크 개수와 일치.


Step 6 — 인스트럭션 RAG 질의

목표: 저장된 문서에 인스트럭션을 걸어 목적별 검색을 수행합니다.

# 일반 유사도 검색
results = vectordb.similarity_search("주택 수도권 전망", k=2)

# 인스트럭션 검색: 같은 "전세"를 다른 task로
for task in [
    "price change (% increase on numeric figure)",
    "risk of jeonse",
    "government measures or institutional changes affecting jeonse",
]:
    print(task, instruct_search(vectordb, task, "전세", k=2))

"전세"라는 한 단어라도 인스트럭션이 "가격 통계 / 리스크 / 정책·제도"냐에 따라 검색 결과의 제목과 내용이 딱 맞게 바뀝니다. 리랭커·BM25 같은 복합 검색 없이도 의도에 부합하는 청크가 나옵니다.

확인: Step 6 완료. 인스트럭션마다 결과 문서가 의도대로 달라짐.


Step 7 — Streamlit 챗봇 app.py 생성

목표: RAG 챗봇 서비스 파일을 Jupyter 매직으로 생성합니다.

# 노트북 셀 맨 위: 이 셀 전체를 app.py 파일로 저장
%%writefile app.py
import os
import chromadb
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

LMSTUDIO_BASE_URL = os.getenv("LMSTUDIO_BASE_URL", "http://host.docker.internal:12345/v1")
LMSTUDIO_API_KEY  = os.getenv("LMSTUDIO_API_KEY", "lm-studio")
EMBED_MODEL       = os.getenv("EMBED_MODEL", "embedding-8b:sl")
LLM_MODEL         = os.getenv("LLM_MODEL", "qwen3.6-35b:mm")
CHROMA_HOST       = os.getenv("CHROMA_HOST", "chromadb")
CHROMA_PORT       = int(os.getenv("CHROMA_PORT", "8000"))

이어서 initialize_chain()에서 리트리버와 프롬프트, 모델을 엮습니다.

retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
template = """당신은 KB 부동산 보고서 전문가입니다. ...컨텍스트: {context}"""
prompt = ChatPromptTemplate.from_messages([
    ("system", template),
    ("placeholder", "{chat_history}"),
    ("human", "{question}"),
])
model = ChatOpenAI(model=LLM_MODEL, base_url=LMSTUDIO_BASE_URL,
                   api_key="lmstudio", temperature=0.7)
base_chain = (
    RunnablePassthrough.assign(
        context=lambda x: format_docs(retriever.invoke(x["question"]))
    ) | prompt | model | StrOutputParser()
)

os.getenv()로 모델·호스트를 환경 변수에서 읽게 해두면, 노트북과 배포 컨테이너가 같은 파일을 공유할 수 있습니다.

⚠️ 주의: 노트북 안에서 app.py를 서브프로세스로 띄우는 것은 재미용입니다. 리버스 프록시로 묶으면 컨테이너 내부 포트라 소켓이 이상하게 동작합니다 — 반드시 별도 Docker로 구워 빼세요.

확인: Step 7 완료. 셀 실행 시 Writing app.py가 출력되고 파일이 생성됨.


Step 마지막 — 배포 + 동작 확인 (테스트)

컨테이너용 Dockerfile(요지):

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.port", "8501"]

배포 순서: ① Streamlit 이미지를 docker-compose up -d --build로 실행(내부 포트 8501) → ② Nginx Proxy Manager 컨테이너 실행(포트 80/443/81) → ③ AP에서 내 PC IP 고정 + 80/443 포트포워딩 → ④ Route 53에 * A레코드로 공인 IP 등록 → ⑤ NPM에서 Let's Encrypt(DNS Challenge, provider route53)로 와일드카드 인증서 발급 → ⑥ Proxy Host를 Forward kb-reg:8501, Websockets Support 활성화, SSL 인증서 연결.

예상 출력 (브라우저에서 https://kb-reg.<도메인> 접속 후 "수도권 주택매매 전망 알려줘"):

[리즈닝 32초] ...
시장 전망: 장기 침체 우려. 지배적 요인은 금리·공급...
권역별: 동탄 …, 강남구 …
(주소창 자물쇠 아이콘 = HTTPS 정상)

5. 적용 사례 (공신력 오픈소스)

  • LangChain (https://github.com/langchain-ai/langchain) — OpenAIEmbeddings·ChatOpenAI·RunnablePassthrough 체인 구성.
  • LlamaIndex (https://github.com/run-llama/llama_index) — 인덱싱·리트리버 추상화 대안.
  • Anthropic Python SDK (https://github.com/anthropics/anthropic-sdk-python) — 생성 LLM을 클라우드로 옮길 때의 표준 클라이언트.
  • OpenAI Python SDK (https://github.com/openai/openai-python) — LM Studio가 흉내내는 OpenAI 호환 API의 원본 스펙.
  • HuggingFace Transformers (https://github.com/huggingface/transformers) — Qwen3·Gemma 계열 임베딩 모델 카드·로딩.
  • vLLM (https://github.com/vllm-project/vllm) — 로컬 임베딩·LLM을 고성능 OpenAI 호환 서버로 서빙하는 대안.
  • Ollama (https://github.com/ollama/ollama) — LM Studio 대체 로컬 모델 런타임.
  • LiteLLM (https://github.com/BerriAI/litellm) — 여러 백엔드를 OpenAI 인터페이스로 통일.
  • Spring AI (https://github.com/spring-projects/spring-ai) / LangChain4j (https://github.com/langchain4j/langchain4j) — JVM 진영에서 같은 RAG 패턴 구현.

📚 참고: 위 OSS는 모두 GitHub stars ≥ 5K 또는 Anthropic·OpenAI·Google 계열 공식 SDK/프로젝트입니다.

💡 실무 노하우: 강의는 LangChain의 OpenAIEmbeddings/ChatOpenAIbase_url만 바꾸면 로컬 서버를 그대로 가리킨다는 "OpenAI 호환 인터페이스" 메타 패턴을 활용합니다. LM Studio → vLLM → Ollama로 백엔드를 갈아끼워도 앱 코드는 그대로입니다.

6. 핵심 원리

가장 중요한 두 가지입니다.

  1. 디코더 임베딩 = 마지막 히든 스테이트: 인코더 임베딩은 토큰 임베딩의 평균이라 문장의 "평균적 의미"만 담아 초점을 잃습니다. 디코더(LLM) 기반은 트랜스포머의 마지막 히든 스테이트를 벡터로 삼아, "과일 값이 비싸다"에서 비싸다가 초점임을 이해합니다. 그래서 벡터 유사도 자체가 정확해집니다.
  2. 인스트럭션 = 검색 관점 스위치: 디코더 임베딩은 인스트럭션(시스템 프롬프트)을 먹으므로, 같은 쿼리를 "가격/리스크/정책" 관점으로 재해석해 임베딩합니다. 검색 로직을 바꾸지 않고 인스트럭션 문자열만 바꿔 결과를 목적에 맞춥니다.

7. 변형·확장

  • 로컬 서버 백엔드 교체: LM Studio 대신 vLLM·Ollama로 OpenAI 호환 임베딩/생성 서버를 띄우고 base_url만 변경.
  • 스트리밍 응답: ChatOpenAI(..., streaming=True)로 토큰 스트리밍 → Streamlit st.write_stream으로 체감 지연 감소.
  • 대화 기억: RunnableWithMessageHistory + ChatMessageHistory로 멀티턴 대화 유지(프롬프트의 {chat_history} 자리).
  • 인스트럭션 세트 자동화: 도메인별 task 딕셔너리를 만들어 사용자 의도 분류 후 자동으로 인스트럭션을 선택.

8. 다른 도구·접근과의 비교 (3-way)

  • vs 인코더 임베딩 + 리랭커/BM25: 구형 스택은 벡터 품질이 낮아 보정 기법이 필수였고 파이프라인이 복잡합니다. 디코더 임베딩은 벡터 검색만으로 대부분 해결되어 구성이 단순합니다.
  • vs 클라우드 임베딩(OpenAI Embeddings API): 클라우드는 설치 부담이 없지만 비용·데이터 유출·오프라인 불가가 단점입니다. 로컬은 초기 자원 투자와 운영 부담이 있으나 프라이버시·비용 우위입니다.
  • vs FAISS 같은 무거운 벡터 라이브러리: 강의는 FAISS를 굳이 쓰지 않고 최소한의 ChromaDB를 권장합니다. 디코더 임베딩과 결합하면 미니멀 벡터DB로도 충분한 성능이 나온다는 관점입니다.

9. 한계·트레이드오프

  1. 자원 요구: 8B 임베딩 + 35B 생성 LLM 동시 구동은 단일 GPU로 벅찹니다. VRAM·RAM이 성능의 병목이며, CPU는 상대적으로 덜 중요합니다.
  2. 215개 일괄 임베딩 부하: 대량 문서를 한 번에 임베딩하면 순간 부하가 큽니다. 배치·비동기 처리와 진행률 관측이 필요합니다.
  3. 온프레미스 보안·안정성: 공인 IP가 유동적이라 바뀌면 Route 53 A레코드를 수동 갱신해야 합니다. 가정용 회선을 외부에 노출하는 것은 DDoS·무단 접근 리스크가 있어 접근 제어가 필요합니다.
  4. "벡터 검색만으로 충분"의 일반화 주의: 강의 도메인(부동산 보고서)에서의 관찰이며, 표·수치·키워드 정확매칭이 중요한 도메인에서는 여전히 BM25·리랭커가 유효할 수 있습니다.
  5. 플랫폼 차이: Mac용 Docker는 WSL2 기반 Windows Docker보다 리눅스 바이너리 호환성이 낮을 수 있어, Mac에서 일부 절차가 그대로 안 될 수 있습니다.

💡 실무 노하우: 에러 처리(retry·fallback), 관측(logging·tracing·프롬프트 버전 관리), rate limit·캐싱은 로컬이라도 반드시 넣으세요. 로컬은 비용은 없어도 자원 고갈로 인한 실패가 잦습니다.

10. 최신 권장 패턴 (2026-08 기준, 강의 및 OSS 공식 문서)

  • OpenAI 호환 인터페이스로 통일: LM Studio·vLLM·Ollama·LiteLLM 모두 OpenAI 호환 엔드포인트를 노출하므로, base_url 교체만으로 백엔드를 바꾸는 이식성 패턴을 기본값으로 삼습니다.
  • 인스트럭션·태스크 프리픽스 임베딩: 디코더 기반 임베딩 모델의 표준 사용법이 "쿼리에 task 인스트럭션을 붙이는 것"으로 자리잡고 있습니다. 모델 카드의 인스트럭션 형식을 그대로 따르세요.
  • 구조화 출력·함수 호출: 생성 단계에서 JSON mode·function calling으로 답변을 구조화하면 후처리가 쉬워집니다.
  • 컨테이너 우선 개발: 로컬을 오염시키지 말고 모든 것을 Docker 컨테이너로 굽고 지지고 볶는 습관 — 배포 환경(리눅스)과 동일해져 재현성이 높아집니다.

11. 메타인지 자기평가

본인 프로젝트에 이 로컬 RAG 패턴을 적용할 수 있는지 점검합니다.

Step 1 — 현재 상태 점검

docker ps                      # ChromaDB·NPM·앱 컨테이너 실행 여부
grep -rn "OpenAIEmbeddings\|as_retriever\|HttpClient" .   # 임베딩·검색 배선 확인
nvidia-smi                     # GPU VRAM 여유 확인

Step 2 — 적용 가능성 평가 - 데이터가 외부로 나가면 안 되는가? → 그렇다면 로컬 RAG의 프라이버시 이점이 큼. - 검색 품질이 낮아 리랭커·BM25로 땜질 중인가? → 디코더 임베딩 교체로 근본 개선 여지. - 쿼리 의도가 다양한가(가격/리스크/정책 등)? → 인스트럭션 검색이 효과적. - 단일 GPU로 8B+35B가 벅찬가? → 임베딩/생성 서버 분산 또는 양자화 필요.

Step 3 — 점진 적용 1. 임베딩만 로컬(디코더 기반)으로 교체하고 기존 검색 파이프라인과 품질 비교. 2. 인스트럭션 검색을 A/B로 도입해 목적별 정확도 측정. 3. 리랭커·BM25를 하나씩 제거하며 품질 저하 여부 확인(무비판 제거 금지 — 도메인 검증). 4. 마지막으로 Docker + NPM으로 HTTPS 배포를 붙여 실제 디플로이 환경까지 검증.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플   P 음성 버전 2026-08-05b